//////////////////////////////////////////////////////////////////////
//
//  Game Source Code
//
//  File: XArea.cpp
//  Description: 2D area class. Area is in XY plane.
//	2D areas manager
//
//  History:
//  - Feb 24, 2002: Created by Kirill Bulatsev
//
//////////////////////////////////////////////////////////////////////
 
#include "StdAfx.h"
#include "Area.h"
#include "AreaManager.h"
#include "ISound.h"

//////////////////////////////////////////////////////////////////////////
//*************************************************************************************************************
//
//			AREAs MANEGER
//
//////////////////////////////////////////////////////////////////////////
//
//////////////////////////////////////////////////////////////////////////
CAreaManager::CAreaManager( CEntitySystem *pEntitySystem )// : CSoundAreaManager(pEntitySystem)
:	m_pEntitySystem(pEntitySystem),
	m_pCurrArea(NULL),
	m_pPrevArea(NULL),
	m_nCurStep(1),
	m_nCurSoundStep(11),
	m_nCurTailStep(22),
	m_bAreasDirty(true)
{
	// Reserve for at least 8 elements.
	m_anAreaIndices.reserve(8);
	
	if (ISystemEventDispatcher* pSystemEventDispatcher = gEnv->pSystem->GetISystemEventDispatcher())
	{
		pSystemEventDispatcher->RegisterListener(this);
	}
}

//////////////////////////////////////////////////////////////////////////
CAreaManager::~CAreaManager()
{
	assert( m_vpAreas.size() == 0 );
	stl::free_container(m_anAreaIndices);

	if (ISystemEventDispatcher* pSystemEventDispatcher = gEnv->pSystem->GetISystemEventDispatcher())
	{
		pSystemEventDispatcher->RemoveListener(this);
	}
}

//////////////////////////////////////////////////////////////////////////
CArea* CAreaManager::CreateArea()
{
	CArea *pArea = new CArea(this);

	m_vpAreas.push_back( pArea );
	return pArea;
}

//////////////////////////////////////////////////////////////////////////
void CAreaManager::Unregister( CArea *pArea )
{
	//invalidating cache and grid
	m_areaCache.clear();
	m_bAreasDirty = true;

	// removing the area reference
	unsigned int const nCount = m_vpAreas.size();
	for (unsigned int i = 0; i < nCount; i++)
	{
		if (m_vpAreas[i] == pArea)
		{
			m_vpAreas.erase( m_vpAreas.begin() + i );
			break;
		}
	}

	if (m_vpAreas.empty())
	{
		stl::free_container(m_vpAreas);
	}
}

//	gets area by point
//////////////////////////////////////////////////////////////////////////
CArea*	CAreaManager::GetArea( const Vec3& point )
{
	float		dist;
	float		closeDist	= -1.0f;
	CArea*	closeArea	= NULL;

	unsigned int const nCount = m_vpAreas.size();
	for (unsigned int aIdx = 0; aIdx < nCount; ++aIdx)
	{
		if (m_vpAreas[aIdx]->GetAreaType() == ENTITY_AREA_TYPE_SHAPE)
		{
			dist = m_vpAreas[aIdx]->CalcPointWithinDist(point, false);
			if (dist>0)
			{
				if ( !closeArea || closeDist>dist )
				{
					closeDist = dist;
					closeArea = m_vpAreas[aIdx];
				}
			}
		}
	}
	return closeArea;
}

//	gets area by index
//////////////////////////////////////////////////////////////////////////
const IArea* CAreaManager::GetArea(int areaIndex) const
{
	CArea *pArea = m_vpAreas[areaIndex];
	return (IArea*)pArea;
}

void CAreaManager::DrawLinkedAreas(EntityId linkedId) const
{
	std::vector<CArea *> areas;
	const unsigned int iNumAreas = GetLinkedAreas(linkedId, -1, areas);
	for (unsigned int iIdx = 0; iIdx < iNumAreas; ++iIdx)
	{
		areas[iIdx]->Draw(iIdx);
	}
}


bool CAreaManager::GetLinkedAreas(EntityId linkedId, EntityId* pOutArray, int &outAndMaxResults) const
{
	int nMaxResults = outAndMaxResults;
	int nArrayIndex = 0;
	unsigned int const nCount = m_vpAreas.size();
	for (unsigned int aIdx = 0; aIdx < nCount; aIdx++)
	{
		if (CArea *pArea = m_vpAreas[aIdx])
		{
			const std::vector<EntityId>& ids=*pArea->GetEntities();
			if (!ids.empty())
			{
				unsigned int const nidCount = ids.size();
				for (unsigned int eIdx = 0; eIdx < nidCount; eIdx++)
				{
					if (ids[eIdx] == linkedId)
					{
						if(nArrayIndex < nMaxResults)
						{
							EntityId areaId = pArea->GetEntityID();
							pOutArray[nArrayIndex] = areaId;
							nArrayIndex++;
						}
						else
						{
							outAndMaxResults = nArrayIndex;
							return false;
						}
					}
				}
			}
		}
	}

	outAndMaxResults = nArrayIndex;
	return true;
}

int CAreaManager::GetLinkedAreas(EntityId linkedId, int areaId, std::vector<CArea *> &areas) const
{
	unsigned int const nCount = m_vpAreas.size();
	for (unsigned int aIdx = 0; aIdx < nCount; aIdx++)
	{
		if (CArea *pArea = m_vpAreas[aIdx])
		{
			const std::vector<EntityId>& ids=*pArea->GetEntities();

			if (!ids.empty())
			{
				unsigned int const nidCount = ids.size();
				for (unsigned int eIdx = 0; eIdx < nidCount; eIdx++)
				{
					if (ids[eIdx] == linkedId)
					{
						if (areaId == -1 || areaId == pArea->GetID())
							areas.push_back(pArea);
					}
				}
			}
		}
	}

	return areas.size();
}

//////////////////////////////////////////////////////////////////////////
void CAreaManager::MarkPlayerForUpdate( EntityId id )
{
	const size_t FramesToUpdate = 1;

	// we could get the same entity for update in one frame -> push_back_unique
	for (size_t i = 0; i < m_playerEntitiesToUpdate.size(); ++i)
	{
		if (m_playerEntitiesToUpdate[i].first == id)
		{
			m_playerEntitiesToUpdate[i].second = FramesToUpdate;

			return;
		}
	}

	m_playerEntitiesToUpdate.push_back(std::make_pair(id, FramesToUpdate));
}

//////////////////////////////////////////////////////////////////////////
void CAreaManager::Update()
{
	unsigned int const nCount = m_playerEntitiesToUpdate.size();

	for (uint32 i = 0; i < nCount; ++i)
	{
		std::pair<EntityId, size_t>& updateEntity = m_playerEntitiesToUpdate[i];
		assert(updateEntity.second);
		--updateEntity.second;

		IEntity *pPlayer = g_pIEntitySystem->GetEntity(updateEntity.first);
		if (pPlayer)
		{
			// Add a Z offset of at least 0.11 to be slightly above the offset of 0.1 set through "CShapeObject::GetShapeZOffset".
			Vec3 const playerPos = pPlayer->GetWorldPos() + Vec3(0.0f, 0.0f, 0.11f);
			UpdatePlayer( playerPos ,pPlayer );

#ifndef _RELEASE
			bool bDrawDebug = CVar::pDrawAreaDebug->GetIVal() != 0;
			if (bDrawDebug)
			{
				float fColor[4]				={1.0f, 1.0f, 1.0f, 0.7f};
				float debugPosY=600;
				gEnv->pRenderer->Draw2dLabel(30, debugPosY, 1.35f, fColor, false, "playerToUpdate=%d (%s) pos=(%.2f, %.2f, %.2f)", updateEntity.first, pPlayer ? pPlayer->GetName() : "NULL", playerPos.x, playerPos.y, playerPos.z);
				debugPosY += 12;
			}
#endif // _RELEASE

		}
	}

	size_t size = m_playerEntitiesToUpdate.size();
	
	for (size_t i = 0; i < size; )
	{
		std::pair<EntityId, size_t>& updateEntity = m_playerEntitiesToUpdate[i];
		if (!updateEntity.second)
		{
			std::swap(updateEntity, m_playerEntitiesToUpdate.back());
			m_playerEntitiesToUpdate.pop_back();
			--size;
			
			continue;
		}
		++i;
	}
}

//	check pPlayer's position for all the areas. Do onEnter, onLeave, updateFade
//////////////////////////////////////////////////////////////////////////
void	CAreaManager::UpdatePlayer( const Vec3 &vPos,IEntity *pPlayer )
{
	FUNCTION_PROFILER( GetISystem(),PROFILE_ENTITY );

	SAreasCache*  pAreaCache = GetAreaCache(pPlayer->GetId());
	if (pAreaCache)
	{
		if (vPos == pAreaCache->vLastUpdatePos)
			return;
		pAreaCache->vLastUpdatePos = vPos;
	}

	// increase the step
	m_nCurStep++;

	// first mark all cache entries that as if they are not in the grid
	if (pAreaCache)
	{
		int const nCacheCount = pAreaCache->Areas.size();

		// Invalidate grid flag in area cache
		for (int nCacheIndex = 0; nCacheIndex < nCacheCount; ++nCacheIndex)
		{
			pAreaCache->Areas[nCacheIndex].bInGrid = false;

			unsigned int nAreaIndex = pAreaCache->Areas[nCacheIndex].nAreaIndex;

			// safe check for editor
			if (nAreaIndex >= m_vpAreas.size())
			{
				assert(!"invald access to area cache");
				return;
			}

			CArea* const pArea = m_vpAreas[nAreaIndex];

			// Now pre-calculate position data
			pArea->InvalidateCachedAreaData();
			pArea->CalcCachedPointPosType(vPos);
		}
	}

	// Update the area grid data structure.
	UpdateDirtyAreas();

	unsigned int nAreaIndex=0;

	uint32 numAreas=0;
	
	uint32 *pAreaList = m_areaGrid.GetAreas(vPos, numAreas);

	for(uint32 i=0; i<numAreas; i++)
	{
		nAreaIndex = pAreaList[i];

		CArea* const pArea = m_vpAreas[nAreaIndex];

		// skip those areas being processed already in the cache
		if (pArea->GetStepId() != m_nCurStep)
		{
			pArea->InvalidateCachedAreaData();

			// Now pre-calculate position data
			pArea->CalcCachedPointPosType(vPos);
		}

		int nCacheIndex = -1;

		// mark cache entries if they are in the grid
		if (pAreaCache)
		{
			nCacheIndex = pAreaCache->GetCacheIndex(nAreaIndex);
			
			if (nCacheIndex != -1)
			{
				pAreaCache->Areas[nCacheIndex].bInGrid = true;
			}
		}

		// if they are not yet in the cache, add them
		if (nCacheIndex == -1)
		{
			// Create a new cache
			if (!pAreaCache)
				pAreaCache = MakeAreaCache(pPlayer->GetId());

			pAreaCache->Areas.push_back(SAreaCacheEntry(nAreaIndex, false, false));
		}
	}

	// go through all cache entries and process the areas
	if (pAreaCache)
	{
		// check all the areas pPlayer is in already
		for (int nCacheIndex = 0; nCacheIndex < static_cast<int>(pAreaCache->Areas.size()); ++nCacheIndex)
		{	
			const SAreaCacheEntry& rAreaCacheEntry = pAreaCache->Areas[nCacheIndex];
			nAreaIndex = rAreaCacheEntry.nAreaIndex;

			// safe check for editor
			if (nAreaIndex >= m_vpAreas.size())
			{
				assert(!"invald access to area cache");
				return;
			}

			CArea* const pArea = m_vpAreas[nAreaIndex];

			// check if Area is hidden
			IEntity const* const pAreaEntity = m_pEntitySystem->GetEntity(pArea->GetEntityID());
			bool bIsHidden = (pAreaEntity && pAreaEntity->IsHidden());

			// area was just hidden
			if (bIsHidden && pArea->IsActive())
			{
				pArea->LeaveArea(pPlayer);
				pArea->LeaveNearArea(pPlayer);
				pArea->SetActive(false);
				continue;
			}

			// area was just unhidden
			if (!bIsHidden && !pArea->IsActive())
			{
				pAreaCache->Areas[nCacheIndex].bNear = false;
				pAreaCache->Areas[nCacheIndex].bInside = false;
				pArea->SetActive(true);
			}

			if (pArea->IsActive())
			{
				ProcessArea(pArea, nAreaIndex, nCacheIndex, pAreaCache, vPos, pPlayer);
#ifndef _RELEASE
				//if the area isn't in the grid
				if(!rAreaCacheEntry.bInGrid)
				{
					//it shouldn't be inside or near
					CRY_ASSERT(rAreaCacheEntry.bInside == false && rAreaCacheEntry.bNear == false);
				}
#endif
			}

		}
	}

	// Go through all Areas again and send the events now
	if (pAreaCache)
	{
		// check all the areas pPlayer is in already
		for (int nCacheIndex = 0; nCacheIndex < static_cast<int>(pAreaCache->Areas.size()); ++nCacheIndex)
		{
			nAreaIndex = pAreaCache->Areas[nCacheIndex].nAreaIndex;

			// safe check for editor
			if (nAreaIndex >= m_vpAreas.size())
			{
				assert(!"invald access to area cache");
				return;
			}

			CArea* const pArea = m_vpAreas[nAreaIndex];
			pArea->SendCachedEventsFor(pPlayer->GetId());
			pArea->SetStepId( m_nCurStep );
		}
	}

	// now lets remove all entries in the cache which are no longer in the grid
	if (pAreaCache)
	{
		// requires re-eval of size due to remove
		for (unsigned int nCacheIndex = 0; nCacheIndex < pAreaCache->Areas.size(); )
		{
			if (!pAreaCache->Areas[nCacheIndex].bInGrid)
			{
				pAreaCache->Areas.erase( pAreaCache->Areas.begin() + nCacheIndex );
			}
			else
				++nCacheIndex;
		}
		
		if (pAreaCache->Areas.empty())
		{
			DeleteAreaCache( pPlayer->GetId() );
		}
	}

}

bool CAreaManager::QueryAreas(Vec3 const& vPos, SAreaManagerResult *pResults, int nMaxResults, bool const bForceCalculation /* = false */)
{
	if (pResults)
	{
		// Make sure the area grid is recompiled, if needed, before accessing it
		UpdateDirtyAreas();

		int nResultCount = 0;
		uint32 numAreas = 0;
			
		uint32 *pAreaList = m_areaGrid.GetAreas(vPos, numAreas);

		for(uint32 i=0; i<numAreas; i++)
		{
			uint32 nAreaIndex = pAreaList[i];
			CArea* const pArea = m_vpAreas[nAreaIndex];

			if (pArea)
			{
				// check if Area is hidden
				IEntity const* const pAreaEntity = m_pEntitySystem->GetEntity(pArea->GetEntityID());

				if (pAreaEntity && !pAreaEntity->IsHidden())
				{
					Vec3 Closest3d(0);
					float fMaxRadius = pArea->GetMaximumEffectRadius();

					float fDistanceSq = 0.0f;
					ECachedAreaData const eAreaFlags = pArea->GetFlags();

					if (bForceCalculation || (eAreaFlags & eCachedAreaData_DistNearSqValid) == 0 || (eAreaFlags & eCachedAreaData_DistNearSqValidNotObstructed) != 0)
					{
						fDistanceSq = pArea->CalcPointNearDistSq(vPos, Closest3d, false);
					}
					else
					{
						fDistanceSq = pArea->GetCachedPointNearDistSq();
					}

					bool bIsPointWithin = false;

					if (bForceCalculation || (eAreaFlags & eCachedAreaData_PosTypeValid) == 0)
					{
						bIsPointWithin = (pArea->CalcCachedPointPosType(vPos, false) == AREA_POS_TYPE_2DINSIDE_ZINSIDE);
					}
					else
					{
						bIsPointWithin = pArea->GetCachedPointPosTypeWithin();
					}

					if (fDistanceSq < fMaxRadius*fMaxRadius || bIsPointWithin)
					{
						// still room to put it in?
						if (nResultCount >= nMaxResults)
							return false;

						// found area that should go into the results
						pResults[nResultCount].pArea = pArea;
						pResults[nResultCount].fDistanceSq = fDistanceSq;
						pResults[nResultCount].vPosOnHull = Closest3d;
						pResults[nResultCount].bInside = bIsPointWithin;
						pResults[nResultCount].bNear = (fDistanceSq < fMaxRadius*fMaxRadius);

						++nResultCount;
					}
				}
			}
		}

		return true;
	}
		
	return false;
}


void CAreaManager::ProcessArea(CArea* const __restrict pArea, int unsigned const nAreaIndex, int const nCacheIndex, SAreasCache* const pAreaCache, Vec3 const& vPos, IEntity const* const pPlayer)
{
	FUNCTION_PROFILER(GetISystem(), PROFILE_ENTITY);

	Vec3 Closest3d;
	bool bExclusiveUpdate = false;
	bool bIsPointWithin   = false;
	ECachedAreaData const eAreaFlags = pArea->GetFlags();

	if (eAreaFlags & eCachedAreaData_PosTypeValid)
	{
		bIsPointWithin = pArea->GetCachedPointPosTypeWithin();
	}
	else		
	{
		bIsPointWithin = (pArea->CalcCachedPointPosType(vPos) == AREA_POS_TYPE_2DINSIDE_ZINSIDE);
	}

	if (bIsPointWithin)
	{
		// was inside/near, now is is inside
		if (!pAreaCache->Areas[nCacheIndex].bInside)
		{
			// We're inside now and not near anymore.
			pArea->EnterArea(pPlayer);
			pAreaCache->Areas[nCacheIndex].bInside = true;
			pAreaCache->Areas[nCacheIndex].bNear   = false;

			// Notify possible lower priority areas about this event.
			NotifyAreas(pAreaCache, nAreaIndex, pPlayer);
		}

		if ((eAreaFlags & eCachedAreaData_DistWithinSqValid) == 0 || (eAreaFlags & eCachedAreaData_DistWithinSqValidNotObstructed) != 0)
		{
			// This updates the area's state.
			pArea->CalcPointWithinDist(vPos, false);
		}

		// if there is no cached event waiting, Fade can be overwritten
		if (pArea->GetStepId() != m_nCurStep)
		{
			pArea->SetFade(1.0f);
		}

		bExclusiveUpdate = ProceedExclusiveUpdateByHigherArea(pAreaCache, pPlayer, nAreaIndex, pArea, vPos);

		if (!bExclusiveUpdate)
		{
			if (pAreaCache->Areas[nCacheIndex].bInside)
			{
				SEntityEvent event(pPlayer->GetId(), 0, 0, 0, 0.0f, 0.0f);
				event.event = ENTITY_EVENT_MOVEINSIDEAREA;
				pArea->AddCachedEvent(event);
			}
		}
	}
	else
	{
		// was inside/near, now is not inside anymore or is hidden
		if (pAreaCache->Areas[nCacheIndex].bInside)
		{
			// We're outside now and near again.
			pArea->LeaveArea(pPlayer);
			pAreaCache->Areas[nCacheIndex].bInside = false;
			pAreaCache->Areas[nCacheIndex].bNear   = true;
			
			// Notify possible lower priority areas about this event.
			NotifyAreas(pAreaCache, nAreaIndex, pPlayer);
		}

		float fDistanceSq = 0.0f;

		// optimization
		if (   eAreaFlags & eCachedAreaData_PosOnHullValid
			&& !(eAreaFlags & eCachedAreaData_PosOnHullValidNotObstructed)
			&& !(eAreaFlags & eCachedAreaData_PosOnHullValidHeightIgnored)
			&& eAreaFlags & eCachedAreaData_DistNearSqValid
			&& !(eAreaFlags & eCachedAreaData_DistNearSqValidNotObstructed)
			&& !(eAreaFlags & eCachedAreaData_DistNearSqValidHeightIgnored))
		{
			Closest3d   = pArea->GetCachedPointOnHull();
			fDistanceSq = pArea->GetCachedPointNearDistSq();
		}
		else
		{
			fDistanceSq = pArea->CalcPointNearDistSq(vPos, Closest3d, false);
		}

		float const fEffectRadius = pArea->GetMaximumEffectRadius();

		if (fDistanceSq < fEffectRadius*fEffectRadius && !bIsPointWithin)
		{
			if (pAreaCache->Areas[nCacheIndex].bNear == false)
			{
				pArea->EnterNearArea(pPlayer, fDistanceSq);
				pAreaCache->Areas[nCacheIndex].bNear = true;
			}

			// is near now
			bExclusiveUpdate = ProceedExclusiveUpdateByHigherArea(pAreaCache, pPlayer, nAreaIndex, pArea, Closest3d);

			// if there is no cached event waiting, Fade can be overwritten
			if (!bExclusiveUpdate)
			{
				pArea->SetFade(1.0f);

				if (!pAreaCache->Areas[nCacheIndex].bInside)
				{
					SEntityEvent event(pPlayer->GetId(), 0, 0, 0, 0.0f, fDistanceSq);
					event.event = ENTITY_EVENT_MOVENEARAREA;
					pArea->AddCachedEvent(event);
					pAreaCache->Areas[nCacheIndex].bNear = true;
				}

			}
		}
		else
		{
			// was near or inside, now is far, or is hidden
			if (pAreaCache->Areas[nCacheIndex].bInside)
			{
				pArea->LeaveArea(pPlayer);
				pAreaCache->Areas[nCacheIndex].bInside = false;
			}

			if (pAreaCache->Areas[nCacheIndex].bNear)
			{
				pArea->LeaveNearArea(pPlayer);
				pAreaCache->Areas[nCacheIndex].bNear = false;
			}
		}
	}

}

//	checks for higher areas in the same group, the player is also in
//	finds multiple point candidates and picks the furthers
//	change fade value the deeper player is inside
//////////////////////////////////////////////////////////////////////////
bool CAreaManager::ProceedExclusiveUpdateByHigherArea(SAreasCache const* const pAreaCache, IEntity const* const pPlayer, int unsigned const nAreaIndex, CArea* const pArea, Vec3 const& vOnLowerHull)
{
	// we try to catch 4 cases here.
	// 1) not-inside-low, not-inside-high: both areas a close, it depends on which area is closer to the player
	// 2) inside-low, not-inside-high: typical approaching high area scenario
	// 3) not-inside-low, inside-high: reversed approach from within a high area to fade in a low area
	// 4) inside-low, inside-high: both inside, so we fade in the lower area on the higher area hull, if that point is also in the lower

	bool bResult = false;
	int const nCurrentGroup = m_vpAreas[nAreaIndex]->GetGroup();

	if (nCurrentGroup > 0)
	{
		// Clear so we have a clean start.
		m_anAreaIndices.clear();

		//	Find areas of higher priority that belong to the same group as the passed pArea.
		int nHigherPrioAreaIndex = -1;
		int const	nMinPriority   = m_vpAreas[nAreaIndex]->GetPriority();

		std::vector<SAreaCacheEntry>::const_iterator const IterEnd(pAreaCache->Areas.end());

		for (std::vector<SAreaCacheEntry>::const_iterator Iter(pAreaCache->Areas.begin()); Iter != IterEnd; ++Iter)
		{
			nHigherPrioAreaIndex = (*Iter).nAreaIndex;

			// Safety check for editor.
			if (nHigherPrioAreaIndex < (int)m_vpAreas.size())
			{
				CArea const* const pHigherPrioArea = m_vpAreas[nHigherPrioAreaIndex];

				// Must be of same group.
				if (nCurrentGroup == pHigherPrioArea->GetGroup())
				{
					// Only check areas which are active (not hidden).
					if (nMinPriority < pHigherPrioArea->GetPriority() && pHigherPrioArea->IsActive())
					{
						m_anAreaIndices.push_back(nHigherPrioAreaIndex);
					}
				}
			}
		}

		// Continue only if there were areas found of higher priority.
		if (!m_anAreaIndices.empty())
		{
			int unsigned const nCacheIndex = pAreaCache->GetCacheIndex(nAreaIndex);
			bool const bPosInLowerArea     = pAreaCache->Areas[nCacheIndex].bInside;

			float fLargestDistanceSq = bPosInLowerArea ? 0.0f : FLT_MAX;
			int nHigherAreaWithLargestDistance = -1;
			float fHigherEffectRadius = 0.0f;
			Vec3 vHigherClosest3d;
			bool bPosInHighestArea = false;
			Vec3 vPlayerPos = pPlayer->GetWorldPos();

			std::vector<int>::const_iterator const IterAreaIndicesEnd(m_anAreaIndices.end());

			for (std::vector<int>::const_iterator IterAreaIndices(m_anAreaIndices.begin()); IterAreaIndices != IterAreaIndicesEnd; ++IterAreaIndices)
			{
				bool bPosInHighArea        = false;
				int const nHigherAreaIndex = (*IterAreaIndices);
				CArea* const pHigherArea   = m_vpAreas[nHigherAreaIndex];

				if (pHigherArea->GetFlags() & eCachedAreaData_PosTypeValid && !(pHigherArea->GetFlags() & eCachedAreaData_PointWithinValidHeightIgnored))
				{
					bPosInHighArea = pHigherArea->GetCachedPointWithin();
				}
				else
				{
					bPosInHighArea = pHigherArea->CalcPointWithin(vPlayerPos);
				}

				float fDistanceSq = 0.0f;
				Vec3 vClosest3d;

				// todo: check if we need to distinguish between inside and near

				// optimization
				ECachedAreaData const eHigherAreaFlags = pHigherArea->GetFlags();
				if (   eHigherAreaFlags & eCachedAreaData_PosOnHullValid
					&& !(eHigherAreaFlags & eCachedAreaData_PosOnHullValidNotObstructed)
					&& !(eHigherAreaFlags & eCachedAreaData_PosOnHullValidHeightIgnored)
					&&   eHigherAreaFlags & eCachedAreaData_DistNearSqValid
					&& !(eHigherAreaFlags & eCachedAreaData_DistNearSqValidNotObstructed)
					&& !(eHigherAreaFlags & eCachedAreaData_DistNearSqValidHeightIgnored))
				{
					vClosest3d  = pHigherArea->GetCachedPointOnHull();
					fDistanceSq = pHigherArea->GetCachedPointNearDistSq();
				}
				else
				{
					fDistanceSq = pHigherArea->CalcPointNearDistSq(vPlayerPos, vClosest3d, false);
				}

				bool bUseThisArea = (fLargestDistanceSq < fDistanceSq && bPosInLowerArea) || (fDistanceSq < fLargestDistanceSq && !bPosInLowerArea);
				// reject cases when Pos is not inside new High Area and we already found a high area
				// and add the case where Pos is inside new High Area, but would be rejected by Higher Area,
				bUseThisArea = bUseThisArea && !(!bPosInHighArea && bPosInHighestArea) || (bPosInHighArea && !bPosInHighestArea);

				if (bUseThisArea)
				{
					fLargestDistanceSq             = fDistanceSq;
					nHigherAreaWithLargestDistance = nHigherAreaIndex;
					fHigherEffectRadius            = pHigherArea->GetMaximumEffectRadius();
					vHigherClosest3d               = vClosest3d;
					bPosInHighestArea              = bPosInHighArea;
				}
			}

			// case 2) if we are in the low area, but not inside the high one, then no need to update exclusive
			if (!bPosInLowerArea || bPosInHighestArea)
			{
				// did we find the best Higher Area to control this Area?
				if (nHigherAreaWithLargestDistance != -1)
				{
					CArea* const pHigherArea              = m_vpAreas[nHigherAreaWithLargestDistance];
					bool const bHighest3dPointInLowerArea = pArea->CalcPointWithin(vHigherClosest3d);
					bool const bLower3dPointInHighestArea = pHigherArea->CalcPointWithin(vOnLowerHull);
					float fFadeDistanceSq                 = fLargestDistanceSq;

					// case 1) where approaching the lower area is closer than the higher
					if (!bPosInLowerArea && !bPosInHighestArea)
					{
						float const fDistanceToLower  = vOnLowerHull.GetSquaredDistance(vPlayerPos);
						float const fDistanceToHigher = vHigherClosest3d.GetSquaredDistance(vPlayerPos);

						if (fDistanceToLower >= fDistanceToHigher && (bHighest3dPointInLowerArea || bLower3dPointInHighestArea))
						{
							// best thing would be to take vOnLowerHull and use that Position to calculate a point
							// on HigherHull, check again, if that would be inside the lower, and fade from there.
							fFadeDistanceSq = FLT_MAX; //fDistanceToLower;
						}
						else
						{
							return false;
						}
					}

					float const fDistance = cry_sqrtf(fFadeDistanceSq);
					float fNewFade        = fHigherEffectRadius > 0.f ? max(0.f, (fHigherEffectRadius - fDistance) / fHigherEffectRadius) : 0.f; 

					// case 4)
					if (bPosInLowerArea && bPosInHighestArea && bHighest3dPointInLowerArea)
					{
						pArea->ExclusiveUpdateAreaInside(pPlayer, pHigherArea->GetEntityID(), fNewFade);
					}
					else
					{
						// if point on higher hull is not the same as point on lower hull, then lets not fade in the effect
						if (bPosInHighestArea && vOnLowerHull.GetDistance(vHigherClosest3d) > 0.01f)
						{
							fNewFade = 0.0f;
						}

						pArea->ExclusiveUpdateAreaNear(pPlayer, pHigherArea->GetEntityID(), fNewFade);
					}

					bResult = true;
				}
			}
		}
	}

	return bResult;
}

//////////////////////////////////////////////////////////////////////////
void CAreaManager::NotifyAreas(SAreasCache const* const pAreaCache, int unsigned const nAreaIndex, IEntity const* const pPlayer)
{
	CArea* const __restrict pArea = m_vpAreas[nAreaIndex];
	int const nCurrentGroup       = pArea->GetGroup();

	if (nCurrentGroup > 0)
	{
		//	Notify all areas of the same group.
		CArea* __restrict pNotifyArea = NULL;
		int nNotifyAreaIndex          = -1;

		std::vector<SAreaCacheEntry>::const_iterator const IterEnd(pAreaCache->Areas.end());

		for (std::vector<SAreaCacheEntry>::const_iterator Iter(pAreaCache->Areas.begin()); Iter != IterEnd; ++Iter)
		{
			nNotifyAreaIndex = (*Iter).nAreaIndex;

			// Skip the current area and do a safety check for the editor.
			if (nNotifyAreaIndex != nAreaIndex && nNotifyAreaIndex < (int)m_vpAreas.size())
			{
				pNotifyArea = m_vpAreas[nNotifyAreaIndex];

				// Must be of same group.
				if (nCurrentGroup == pNotifyArea->GetGroup())
				{
					// Only check areas which are active (not hidden).
					if (pNotifyArea->IsActive())
					{
						pNotifyArea->OnAreaCrossing(pPlayer);
					}
				}
			}
		}
	}

	OnEvent(ENTITY_EVENT_CROSS_AREA, pPlayer->GetId(), pArea);
}

//	do onexit for all areas pPlayer is in - do it before kill pPlayer
//////////////////////////////////////////////////////////////////////////
void	CAreaManager::ExitAllAreas( IEntity const* const pPlayer )
{
	SAreasCache *pAreaCache = GetAreaCache(pPlayer->GetId());

	if (pAreaCache)
	{
		// check all the areas pPlayer is in already
		unsigned int nCount = pAreaCache->Areas.size();
		for (unsigned int inIdx = 0; inIdx < nCount; inIdx++)
		{
			if (pAreaCache->Areas[inIdx].bInside)
			{
				unsigned int aIdx;
				aIdx = pAreaCache->Areas[inIdx].nAreaIndex;
				// was inside, now is out - do OnLeaveArea
				if (aIdx<m_vpAreas.size())
				{
					m_vpAreas[aIdx]->LeaveArea( pPlayer );
					m_vpAreas[aIdx]->LeaveNearArea( pPlayer );
					pAreaCache->Areas.erase( pAreaCache->Areas.begin() + inIdx );
					inIdx--;
					nCount--;
				}
			}
		}
		DeleteAreaCache( pPlayer->GetId() );
	}
}

//////////////////////////////////////////////////////////////////////////
void	CAreaManager::DrawAreas(const ISystem * const pSystem) 
{
#ifndef _RELEASE
	bool bDrawAreas = CVar::pDrawAreas->GetIVal() != 0;

	if (bDrawAreas)
	{
		unsigned int const nCount = m_vpAreas.size();
		for (unsigned int aIdx = 0; aIdx < nCount; aIdx++)
			m_vpAreas[aIdx]->Draw( aIdx );
	}

	m_fDebugPosY = 200;
	bool bDrawDebug = CVar::pDrawAreaDebug->GetIVal() != 0;

	float fColor[4]				={1.0f, 1.0f, 1.0f, 0.7f};
	if (bDrawDebug)
	{
		const TAreaCacheMap::const_iterator ItEnd = m_areaCache.end();
		TAreaCacheMap::iterator It = m_areaCache.begin();

		for (It; It != ItEnd; ++It)
		{
			IEntity *pPlayer = gEnv->pEntitySystem->GetEntity((*It).first);

			if (pPlayer)
			{

				SAreasCache AreaCache = (*It).second;
				
				gEnv->pRenderer->Draw2dLabel(30, m_fDebugPosY, 1.35f, fColor, false, "[%s]", pPlayer->GetName());
				m_fDebugPosY += 12;
				
				gEnv->pRenderer->Draw2dLabel(30, m_fDebugPosY, 1.35f, fColor, false, "Num entries in cache: %d total areas: %d", AreaCache.Areas.size(), m_areaGrid.GetNumAreas());
				m_fDebugPosY += 12;

				{
					int const nCacheCount = AreaCache.Areas.size();
					Vec3 vPos = AreaCache.vLastUpdatePos;

					// Invalidate grid flag in area cache
					for (int nCacheIndex = 0; nCacheIndex < nCacheCount; ++nCacheIndex)
					{
						unsigned int nAreaIndex = AreaCache.Areas[nCacheIndex].nAreaIndex;

						// safe check for editor
						if (nAreaIndex >= m_vpAreas.size())
						{
							assert(!"invald access to area cache");
							return;
						}

						CArea* const pArea = m_vpAreas[nAreaIndex];

						float fDistanceSq = 0.0f;
						if (pArea->GetFlags() & eCachedAreaData_DistNearSqValid && !(pArea->GetFlags() & eCachedAreaData_DistNearSqValidNotObstructed))
							fDistanceSq = pArea->GetCachedPointNearDistSq();
						else
							fDistanceSq = pArea->CalcPointNearDistSq(vPos, false, false);

						bool bIsPointWithin = false;
						if (pArea->GetFlags() & eCachedAreaData_PosTypeValid)
							bIsPointWithin = pArea->GetCachedPointPosTypeWithin();
						else
							bIsPointWithin = (pArea->CalcCachedPointPosType(vPos) == AREA_POS_TYPE_2DINSIDE_ZINSIDE);

						float fMaxRadius = pArea->GetMaximumEffectRadius();
						bool bIsnear = (fDistanceSq < fMaxRadius*fMaxRadius);

						string sState = bIsPointWithin ? "Inside" : (bIsnear ? "Near" : "Far");

						if (bIsnear || bIsPointWithin)
						{
							gEnv->pRenderer->Draw2dLabel(30, m_fDebugPosY, 1.35f, fColor, false, "AreaIndex :%d Priority :%d Distance:%.2f State :%s", nAreaIndex, pArea->GetPriority(), cry_sqrtf(fDistanceSq), sState.c_str() );
							m_fDebugPosY += 10;
						}
					}
				}
			}
		}
	}
#endif // _RELEASE
}

//////////////////////////////////////////////////////////////////////////
void CAreaManager::DrawGrid()
{
	m_areaGrid.Draw();
}

//////////////////////////////////////////////////////////////////////////
unsigned CAreaManager::MemStat()
{
	unsigned memSize = sizeof *this;

	unsigned int const nCount = m_vpAreas.size();
	for (unsigned int aIdx = 0; aIdx < nCount; aIdx++)
		memSize += m_vpAreas[aIdx]->MemStat();

	return memSize;
}

void CAreaManager::ResetAreas()
{
	for (TAreaCacheMap::iterator cacheIt = m_areaCache.begin(), cacheItEnd = m_areaCache.end();
		cacheIt != cacheItEnd;
		++ cacheIt)
	{
		int entityId = cacheIt->first;
		SAreasCache* pCache = &cacheIt->second;

		IEntity* pEntity = m_pEntitySystem->GetEntity(entityId);
		if (pEntity)
		{
			const std::vector<SAreaCacheEntry>::const_iterator cEnd = pCache->Areas.end();
			for(std::vector<SAreaCacheEntry>::const_iterator iter = pCache->Areas.begin(); iter != cEnd; ++iter)
			{
				const SAreaCacheEntry& cacheEntry = *iter;

				if (cacheEntry.bInside)
				{
					CArea* pArea = m_vpAreas[cacheEntry.nAreaIndex];
					pArea->LeaveArea(pEntity);
				}
				if (cacheEntry.bInside || cacheEntry.bNear)
				{
					CArea* pArea = m_vpAreas[cacheEntry.nAreaIndex];
					pArea->LeaveNearArea(pEntity);
				}
			}
		}
	}

	m_areaCache.clear();
	m_anAreaIndices.clear();
	stl::free_container(m_playerEntitiesToUpdate);

	// invalidate cached event
	unsigned int const nCount = m_vpAreas.size();
	for (unsigned int i = 0; i < nCount; i++)
	{
		m_vpAreas[i]->ClearCachedEvents();
	}
}

void CAreaManager::UnloadAreas()
{
	ResetAreas();
	m_areaGrid.Reset();
}

//////////////////////////////////////////////////////////////////////////
void CAreaManager::SetAreasDirty()
{
	m_bAreasDirty = true;
}

//////////////////////////////////////////////////////////////////////////
void CAreaManager::UpdateDirtyAreas()
{
	if (m_bAreasDirty)
	{
		m_areaGrid.Compile(m_pEntitySystem, m_vpAreas);

		m_bAreasDirty = false;
	}
}

//------------------------------------------------------------------------
void CAreaManager::AddEventListener(IAreaManagerEventListener *pListener)
{
	stl::push_back_unique(m_EventListeners, pListener );
}

//------------------------------------------------------------------------
void CAreaManager::RemoveEventListener(IAreaManagerEventListener *pListener)
{
	stl::find_and_erase(m_EventListeners, pListener);
}

//////////////////////////////////////////////////////////////////////////
void CAreaManager::OnEvent( EEntityEvent event, EntityId TriggerEntityID, IArea *pArea )
{
	if (!m_EventListeners.empty())
	{
		TAreaManagerEventListenerVector::iterator ItEnd = m_EventListeners.end();
		for (TAreaManagerEventListenerVector::iterator It = m_EventListeners.begin(); It != ItEnd; ++It)
		{
			assert (*It);
			(*It)->OnAreaManagerEvent( event, TriggerEntityID, pArea);
		}
	}
}

//------------------------------------------------------------------------
int CAreaManager::GetNumberOfPlayersInArea( const CArea *pArea ) const
{
	// Find the area index
	const int numAreasTotal = m_vpAreas.size();
	for (int nAreaIndex = 0; nAreaIndex < numAreasTotal; ++ nAreaIndex)
	{
		if (m_vpAreas[nAreaIndex] == pArea)
		{
			int numPlayersInArea = 0;

			// Now find how many players are actually inside the area
			TAreaCacheMap::const_iterator it;
			TAreaCacheMap::const_iterator endIt = m_areaCache.end();
			for (it = m_areaCache.begin(); it != endIt; ++ it)
			{
				const SAreasCache &areaCache = it->second;
				const int cacheIndex = areaCache.GetCacheIndex(nAreaIndex);

				if ((cacheIndex != -1) && (areaCache.Areas[cacheIndex].bInside))
				{
					++ numPlayersInArea;
				}
			}
			return numPlayersInArea;
		}
	}

	// If we get here then we've failed to find the area - should never happen
	return 0;
}

void CAreaManager::OnSystemEvent(ESystemEvent event, UINT_PTR wparam, UINT_PTR lparam)
{
	switch(event)
	{
	case ESYSTEM_EVENT_SHIFT_WORLD:
		const int numAreasTotal = m_vpAreas.size();
		for(int nAreaIndex = 0; nAreaIndex < numAreasTotal; ++ nAreaIndex)
		{
			m_vpAreas[nAreaIndex]->ResolveEntityIds();
		}
		break;
	}
}

#include UNIQUE_VIRTUAL_WRAPPER(IAreaManager)
